<?php

namespace App\Utility;

use easySwoole\Cache\Cache;
use EasySwoole\EasySwoole\Config;
use EasySwoole\EasySwoole\Logger;
use EasySwoole\HttpClient\HttpClient;
use EasySwoole\Spl\SplString;
use EasySwoole\Utility\File;

class MiniProgram{
    const API_BASE = 'https://api.weixin.qq.com/';
    const API_DOWN = 'http://file.api.weixin.qq.com/';
    const API_OPEN = 'https://open.weixin.qq.com/';

    protected $app;
    //get请求带上?，post不用
    protected $apis = [
        'access_token' => self::API_BASE.'cgi-bin/token?',       //获取access_token
        'session_key'  => self::API_BASE.'sns/jscode2session?',  //获取小程序session_key
        'send_template_message' => self::API_BASE.'cgi-bin/message/template/send',  //推送模板消息
        'send_custom_message' => self::API_BASE.'cgi-bin/message/custom/send',  //发送客服消息
        'template_list' => self::API_BASE.'cgi-bin/wxopen/template/list', //模板消息列表
        'menu_create' => self::API_BASE.'cgi-bin/menu/create', //菜单创建
        'menu_get' => self::API_BASE.'cgi-bin/menu/get?', //菜单查询
        'wechat_ticket' => self::API_BASE.'cgi-bin/ticket/getticket?',
        'wechat_session_key' => self::API_BASE.'sns/oauth2/access_token?',
        'wechat_userinfo' => self::API_BASE.'sns/userinfo?',
        'get_media' => self::API_BASE.'cgi-bin/media/get?',
        'down_media' => self::API_DOWN.'cgi-bin/media/get?',
    ];
    protected $cachePrefix = 'applet.';
    protected $safeSeconds = 800;
    protected $required = ['touser', 'template_id', 'form_id'];
    function __construct(){
        $this->app = Config::getInstance()->getConf('WECHAT');
    }

    /**
     * 基本配置
     * @return array
     */
    protected function getConfig(): array{
        return [
            'appid' => $this->app['appid'],
            'secret' => $this->app['secret'],
        ];
    }

    /**
     * 根据openid获取登录用户的信息
     * @param $data
     * @return mixed
     * @throws \Exception
     */
    public function wechatUserInfo($data){
        $params = ['access_token' => $data['access_token'],'openid' => $data['openid']];
        $result = $this->sendRequest('GET',$this->apis['wechat_userinfo'],$params);
        if(isset($result['errcode']) && $result['errcode']){
            throw new \Exception($this->errorToMsg($result));
        }
        return $result;
    }


    /**
     * 公众号授权登录通过code，换取openid及token信息
     * @param $code
     * @return mixed
     * @throws \Exception
     */
    public function wechatUserToken($code){
        $config = $this->getConfig();
        $params = array_merge(['code' => $code,'grant_type'=>'authorization_code'],$config);
        $result = $this->sendRequest('GET',$this->apis['wechat_session_key'],$params);
        if(isset($result['errcode']) && $result['errcode']){
            throw new \Exception($this->errorToMsg($result));
        }
        return $result;
    }

    /**
     * 获取jsapi_ticket
     * @return mixed
     * @throws \Exception
     */
    public function getTicket(){
        $cache = $this->getCache();
        $cacheKey = $this->getCacheKey('jsapi_ticket');
        //先从缓存读取
        $ticketInfo = $cache::get($cacheKey);
        if(!$ticketInfo){
            $token = $this->getToken();
            $params = ['type'=>'jsapi','access_token'=>$token];
            $ticketInfo = $this->sendRequest('GET',$this->apis['wechat_ticket'],$params);
            if($ticketInfo['errcode']){
                throw new \Exception($this->errorToMsg($ticketInfo));
            }
            $this->getCache()::set($cacheKey, $ticketInfo, $ticketInfo['expires_in']-$this->safeSeconds);
        }
        return $ticketInfo['ticket'];

    }

    /**
     * 获取token
     * @return null
     * @throws \Exception
     */
    public function getToken($refresh=false){
        if($refresh){
            $token = $this->requestToken();
            //设置token缓存
            $this->setToken($token);
            return $token['access_token'];
        }
        $cache = $this->getCache();
        $cacheKey = $this->getCacheKey();
        //先从缓存读取
        $token = $cache::get($cacheKey);
        if(!$token){
            $token = $this->requestToken();
            //设置token缓存
            $this->setToken($token);
        }
        return $token['access_token'];
    }

    /**
     * 请求token
     * @return mixed
     * @throws \Exception
     */
    protected function requestToken(){
        $config = $this->getConfig();
        $params = array_merge(['grant_type'=>'client_credential'],$config);
        $token = $this->sendRequest('GET',$this->apis['access_token'],$params);
        if(isset($token['errcode']) && $token['errcode']){
            throw new \Exception($this->errorToMsg($token));
        }
        return $token;
    }

    /**
     * 保存token
     * @param $token
     * @return bool
     * @throws \Exception
     */
    protected function setToken($token){
        $this->getCache()::set($this->getCacheKey(), $token, $token['expires_in']-$this->safeSeconds);
        return true;
    }


    /**
     * 发送模板消息
     * @param $openid
     * @param $order
     * @param int $type
     * @return mixed
     * @throws \Exception
     */
    public function sendTemplateMessage($openid,$order,$type=1){
        //发送模板消息
        $token = $this->getToken();
        $params = $this->getMessageContent($openid,$order,$type);
        //检查必须填写的字段
        foreach ($params as $key => $value) {
            if (in_array($key, $this->required, true) && empty($value)) {
                throw new \Exception(sprintf('字段: "%s" 内容不能为空!', $key));
            }
        }
        //格式化模板消息内容
        $params['data'] = $this->formatData($params['data']);
        $params = $this->formatMessage($params);
        return $this->sendRequest('POST',$this->apis['send_template_message'].'?access_token='.$token,$params);
    }

    /**
     * 获取模板消息内容
     * @param $openid
     * @param $order
     * @param $type
     * @return array
     */
    protected function getMessageContent($openid,$order,$type){
        $message = [];
        switch ($type){
            case 1:
                //通知管理员新订单生成
                $message = [
                    'touser' => $openid,
                    'template_id' => 'daLiC5qo50JxE-NixWYaR2xqAmJi0vMVh9lOHKOjulg',
                    'url' => '',
                    'data' => [
                        'first' => '您好，收到一条新的未处理工单请及时处理：',
                        'keyword1' => $order['order_id'],
                        'keyword2' => $order['title'],
                        'keyword3' => $order['mobile'],
                        'keyword4' => $order['username'],
                        'keyword5' => $order['address'],
                        'remark' => '请尽快登录管理后台分配维修人员及时联系用户',
                    ],
                ];
                break;
            case 2:
                //通知维修人员处理订单
                $message = [
                    'touser' => $openid,
                    'template_id' => 'daLiC5qo50JxE-NixWYaR2xqAmJi0vMVh9lOHKOjulg',
                    'url' => $order['page'],
                    'data' => [
                        'first' => '您好，收到一条新的未处理工单请及时处理：',
                        'keyword1' => $order['order_id'],
                        'keyword2' => $order['title'],
                        'keyword3' => $order['mobile'],
                        'keyword4' => $order['username'],
                        'keyword5' => $order['address'],
                        'remark' => '点击查看工单详情，请尽快联系用户并上门处理',
                    ],
                ];
                break;
            case 3:
                //维修人员上报维修情况
                $message = [
                    'touser' => $openid,
                    'template_id' => 'cNG3MwBlpvyuj3JelR8AhVuQqCt2etziGmTestfHtyo',
                    'url' => '',
                    'data' => [
                        'first' => '您好，收到一条新的未处理工单请及时处理：',
                        'keyword1' => $order['title'],
                        'keyword2' => $order['order_id'],
                        'remark' => '请尽快登录管理后台查看该订单维修情况及时回复',
                    ],
                ];
                break;
            case 4:
                $message = [
                    'touser' => $openid,
                    'template_id' => 'f20qQkGM5o3bjDCUezADTIeNPliglhcjHSKbd_6v2hs',
                    'url' => $order['page'],
                    'data' => [
                        'first' => '您好，收到一条新的订单受理通知：',
                        'keyword1' => $order['title'],
                        'keyword2' => $order['order_id'],
                        'keyword3' => date('Y-m-d H:i:s',time()),
                        'keyword4' => '同意维修',
                        'remark' => '点击查看工单详情',
                    ],
                ];
                break;
            case 5:
                $message = [
                    'touser' => $openid,
                    'template_id' => 'f20qQkGM5o3bjDCUezADTIeNPliglhcjHSKbd_6v2hs',
                    'url' => $order['page'],
                    'data' => [
                        'first' => '您好，收到一条新的订单受理通知：',
                        'keyword1' => $order['title'],
                        'keyword2' => $order['order_id'],
                        'keyword3' => date('Y-m-d H:i:s',time()),
                        'keyword4' => '拒绝维修',
                        'remark' => '点击查看工单详情',
                    ],
                ];
                break;
        }
        return $message;
    }



    /**
     * 格式化模板消息内容（满足小程序和公众号）
     * @param array $data
     * @return array
     */
    protected function formatData($data){
        $formatted = [];
        foreach ($data as $key => $value) {
            if (is_array($value)) {
                if(isset($value['value'])) {
                    $formatted[$key] = $value;
                    continue;
                }
                if(count($value) >= 2) {
                    $value = [
                        'value' => $value[0],
                        'color' => $value[1],
                    ];
                }
            }else{
                $value = ['value' => $value];
            }
            $formatted[$key] = $value;
        }
        return $formatted;
    }

    /**
     * send a http request
     * @param string $method
     * @param string $url
     * @param mixed $params
     * @return mixed
     */
    protected function sendRequest($method='GET',$url='',$params=null){
        if($method == 'GET') $url .= http_build_query($params);
        $request = new HttpClient($url);
        if($method == 'POST')  $request->postJSON($params);
        $result = $request->exec();
        $string = new SplString($result->getBody());
        return json_decode($string, true);
    }

    /**
     * 获取缓存
     * @return Cache
     * @throws \Exception
     */
    protected function getCache(){
        return new Cache();
    }

    /**
     * 获取缓存前缀
     * @param string $key
     * @return string
     */
    protected function getCacheKey($key = 'access_token'){
        return $this->cachePrefix.'.'.$key;
    }

    /**
     * 验证token
     * @param $params
     * @return bool
     */
    public function service($params){
        if($this->checkSignature($params)){
            $echoStr = $params["echostr"];
            return $echoStr;
        }
        return false;
    }

    /**
     * 签名校验
     * @param $params
     * @return bool
     */
    protected function checkSignature($params){
        $signature = $params["signature"];
        $timestamp = $params["timestamp"];
        $nonce = $params["nonce"];
        $token = $this->app['token'];
        $tmpArr = [$token, $timestamp, $nonce];
        sort($tmpArr, SORT_STRING);
        $tmpStr = implode($tmpArr);
        $tmpStr = sha1($tmpStr);
        if($tmpStr == $signature){
            return true;
        }
        return false;
    }

    /**
     * 获取HTTP_RAW_POST_DATA
     * @param $body
     * @return mixed
     */
    public function getRaw($body){
        $content = $body->__toString();
        return json_decode($content, true);
    }

    /**
     * 发送客服消息
     * @param $data
     * @return bool|string
     * @throws \Exception
     */
    public function sendCustomerMessage($data){
        $token = $this->getToken();
        $params = $this->formatMessage($data);
        return $this->sendRequest('POST',$this->apis['send_custom_message'].'?access_token='.$token,['body' => $params]);
    }


    /**
     * 格式化消息内容
     * @$data['type']  文本消息|图片消息|图片消息|图文消息|视频消息|声音消息|小程序卡片
     * @param $data
     * @return array|string
     */
    protected function formatMessage($data){
        if(isset($data['type']) && $data['type']){
            switch ($data['type']){
                case 'text'://文本消息
                    /**
                     * content内容：文本/a标签
                     * <a href="xxxx"> 跳转链接</a>
                     * <a data-miniprogram-appid="xxxx" data-miniprogram-path="xxxx"> 点击跳小程序</a>
                     * data-miniprogram-appid 项，填写小程序appid，则表示该链接跳转小程序，data-miniprogram-appid必须是当前小程序的appid；
                     * data-miniprogram-path项，填写小程序路径，路径与app.json中保持一致，可带参数；
                     * 对于不支持data-miniprogram-appid 项的客户端版本，如果有herf项，则仍然保持跳href中的链接；
                     */
                    $message = [
                        "touser" => $data['openid'],//用户的 OpenID
                        "msgtype" => 'text',//消息类型
                        "text"  => [
                            'content' => $data['content'],//文本消息内容，可以是a标签
                        ],
                    ];
                    break;
                case 'image'://图片消息
                    $message = [
                        "touser" => $data['openid'],//用户的 OpenID
                        "msgtype" => 'image',//消息类型
                        "image"  => [
                            'media_id' => $data['media_id'],//发送的图片的媒体ID(需要通过接口uploadTempMedia上传图片到微信服务器)
                        ],
                    ];
                    break;
                case 'link'://图文消息
                    $message = [
                        "touser" => $data['openid'],
                        "msgtype" => 'link',
                        "link"  => [
                            'title' => $data['title'],//消息标题
                            'description' => $data['description'],//图文链接消息描述
                            'url' => $data['url'],//图文链接消息被点击后跳转的链接
                            'thumb_url' => $data['thumb_url'],//图文链接消息的图片链接，支持 JPG、PNG 格式，较好的效果为大图 640 X 320，小图 80 X 80
                        ],
                    ];
                    break;
                case 'video'://视频消息
                    break;
                case 'voice'://声音消息
                    break;
                case 'miniprogrampage'://小程序卡片
                    $message = [
                        "touser" => $data['openid'],
                        "msgtype" => 'link',
                        "link"  => [
                            'title' => $data['title'],//消息标题
                            'pagepath' => $data['pagepath'],//小程序的页面路径，跟app.json对齐，支持参数，比如pages/index/index?foo=bar
                            'thumb_media_id' => $data['thumb_media_id'],//小程序消息卡片的封面（需要通过接口uploadTempMedia上传图片到微信服务器）
                        ],
                    ];
                    break;
                default:
                    break;
            }
        }else{
            $message = $data;
        }
        return json_encode($message,JSON_UNESCAPED_UNICODE);
    }

    /**
     * 生成随机字符串
     * @param int $length
     * @return string
     */
    public function createNonceStr($length = 16) {
        $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        $str = "";
        for ($i = 0; $i < $length; $i++) {
            $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
        }
        return $str;
    }

    /**
     * 创建菜单
     * @param $data
     * @return bool|string
     * @throws \Exception
     */
    public function createMenu($params){
        $token = $this->getToken();
        $params = json_encode($params,JSON_UNESCAPED_UNICODE);
        $result = $this->sendRequest('POST',$this->apis['menu_create'].'?access_token='.$token,$params);
        if($result['errcode']){
            throw new \Exception($this->errorToMsg($result));
        }
        return true;
    }

    /**
     * 获取临时素材
     * @param string $media_id
     * @return mixed
     * @throws \Exception
     */
    public function downMedia($media_id = ''){
        if(empty($media_id)){
            throw new \Exception($this->errorToMsg('临时素材媒体文件ID不能为空'));
        }
        $token = $this->getToken();
        $params = [
            'access_token' => $token,
            'media_id' => $media_id,
        ];
        $url = $this->apis['down_media'].http_build_query($params);
        $result = file_get_contents($url);
        $result = json_decode($result,true);
        if(isset($result['errcode']) && $result['errcode']){
            throw new \Exception($this->errorToMsg($result));
        }
        $fileName = '/upload/'.date('Y-m-d').'/'.hash('md5',$media_id).'.jpg';
        $filePath = Config::getInstance()->getConf('STATIC_ROOT').'/'.$fileName;
        $res = File::createFile($filePath,file_get_contents($url));
        if($res === false){
            throw new \Exception($this->errorToMsg('上传图片失败'));
        }
        $domain = Config::getInstance()->getConf('SERVER_NRL');
        return $domain.trim($fileName,'/');
    }


    /**
     *  微信errcode转化
     * @param $result
     * @return mixed
     * @throws \Exception
     */
    protected function errorToMsg($result){
        switch ($result['errcode']){
            case 40001:
                $this->getToken(true);
                $msg = '获取access_token时AppSecret错误，或者access_token无效';
                break;
            case 40002:
                $msg = '不合法的凭证类型';
                break;
            case 40003:
                $msg = '不合法的OpenID';
                break;
            case 40007:
                $msg = '无效的媒体ID';
                break;
            case 40037:
                $msg = 'template_id不正确';
                break;
            case 40018:
                $msg = '无效菜单名长度,最长不超过16个字节';
                break;
            case 40163:
                $msg = 'code已经被使用';
                break;
            case 40164:
                $msg = 'IP白名单错误，'.$result['errmsg'];
                break;
            case 41001:
                $msg = '缺失access_token参数';
                break;
            case 41028:
                $msg = 'form_id无效或者已过期';
                break;
            case 41029:
                $msg = 'form_id已被使用';
                break;
            case 41030:
                $msg = 'page不正确';
                break;
            case 44002:
                $msg = 'post数据为空';
                break;
            case 45009:
                $msg = '小程序模板消息推送接口调用超过限额（目前默认每个帐号日调用限额为100万）';
                break;
            case 45015:
                $msg = '回复时间超过限制';
                break;
            case 45047:
                $msg = '客服接口下行条数超过上限';
                break;
            case 45086:
                $msg = '无效的data-miniprogram-appid标签，必须填写当前的小程序appid';
                break;
            case 48001:
                $msg = 'API功能未授权，请确认小程序已获得该接口';
                break;
            default:
                $msg = $result['errmsg'];
                break;
        }
        return '状态码：'.$result['errcode'].'，提示：'.$msg;
    }
}
